package com.coolpad.Utils;

import org.apache.commons.lang3.tuple.Pair;

import java.util.HashSet;
import java.util.Set;


/**
 * @author qifuguang
 * @date 16-11-9 下午4:54
 * <p>
 * GEO编码工具类.
 */
public class GeoHashUtils {
    // geohash base32编码长度与误差
    // 长度为8时，精度在19米
    // 长度为9时，精度在4.7米
    // 长度为10时，精度在0.6米
    private static final String[] charSet = new String[]{
            "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "b", "c", "d", "e", "f", "g",
            "h", "j", "k", "m", "n", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"
    };
    private static final String charSetStr = "0123456789bcdefghjkmnpqrstuvwxyz";

    private static final double MAX_LONGITUDE = 180.0;
    private static final double MIN_LONGITUDE = -180.0;
    private static final double MAX_LATITUDE = 90.0;
    private static final double MIN_LATITUDE = -90.0;
    private static final double METERS_PER_LATITUDE = 111319.55;

    public static final int[] GEO_FENCING_PRECISION = {2500000, 630000, 78000, 20000, 2400, 610, 76, 19};

    public final class Constants {
        public static final int DEFAULT_GEO_HASH_CODE_LEN = 10;
        public static final int SCAN_CACHING_NUMBER = 1000;
        public static final String SPLITTER = "~";
        public static final int GEO_FENCING_MAX_RADIUS = 50000;
        public static final int GEO_FENCING_MIN_RADIUS = 50;

        public static final int FILTER_VECTOR_SIZE = 20971520;

        public static final double X_PI = 3.14159265358979324 * 3000.0 / 180.0;
        public static final double PI = 3.1415926535897932384626;
        public static final double A = 6378245.0;
        public static final double EE = 0.00669342162296594323;

        public static final double LATITUDE_CENTER_CN = 30.0002;
        public static final double LONGITUDE_CENTER_CN = 104;
    }
    // 地球半径
    private static final double EARTH_RADIUS = 6378.137;

    public static String geoHash(double longitude, double latitude) {
        return geoHash(longitude, latitude, Constants.DEFAULT_GEO_HASH_CODE_LEN);
    }

    public static String geoHash(String longitude, String latitude) {
        return geoHash(Double.valueOf(longitude),Double.valueOf(latitude));
    }

    public static Pair<Double, Double> fromGeoHash(final String geoHash) {
        if (geoHash == null || geoHash.isEmpty()) {
            return null;
        }

        boolean evenBit = true;
        double latMin = -90;
        double latMax = 90;
        double lonMin = -180;
        double lonMax = 180;

        for (int i = 0; i < geoHash.length(); i++) {
            char chr = Character.toLowerCase(geoHash.charAt(i));
            int idx = charSetStr.indexOf(chr);
            if (idx == -1) {
                return null;
            }

            for (int j = 4; j >= 0; j--) {
                int bitN = idx >> j & 1;
                if (evenBit) {
                    // longitude
                    double lonMid = (lonMin + lonMax) / 2;
                    if (bitN == 1) {
                        lonMin = lonMid;
                    } else {
                        lonMax = lonMid;
                    }
                } else {
                    // latitude
                    double latMid = (latMin + latMax) / 2;
                    if (bitN == 1) {
                        latMin = latMid;
                    } else {
                        latMax = latMid;
                    }
                }
                evenBit = !evenBit;
            }
        }
        double lat = (latMin + latMax) / 2;
        double lon = (lonMin + lonMax) / 2;

        return Pair.of(lon, lat);
    }

    public static String geoHash(double longitude, double latitude, int hashLength) {
        int totalLength = hashLength * 5;
        int longitudeLength = (totalLength >> 1) + (totalLength & 1);
        int latitudeLength = totalLength >> 1;
        String longitudeCode = getCode(longitude, MIN_LONGITUDE, MAX_LONGITUDE, longitudeLength);
        String latitudeCode = getCode(latitude, MIN_LATITUDE, MAX_LATITUDE, latitudeLength);
        StringBuilder allCode = new StringBuilder();
        for (int i = 1; i <= totalLength; i++) {
            int index = (i - 1) >> 1;
            if ((i & 1) == 1) {  // 奇数，经度
                allCode.append(longitudeCode.charAt(index));
            } else {
                allCode.append(latitudeCode.charAt(index));
            }
        }
        String geoCode = allCode.toString();
        StringBuilder result = new StringBuilder();
        for (int i = 0; i < hashLength; i++) {
            result.append(charSet[Integer.valueOf(geoCode.substring(i * 5, (i + 1) * 5), 2)]);
        }
        return result.toString();
    }

    public static int getGeoScale(double radius) {
        int scale = 7;
        for (int i = 0; i < GEO_FENCING_PRECISION.length; i++) {
            if ((int) (radius / GEO_FENCING_PRECISION[i]) > 0) {
                scale = i;
                break;
            }
        }
        return scale;
    }

    public static Set<String> geoHash(double longitude, double latitude, int hashLength, double radius) {
        /**
         * 在经线上，相差一纬度约为111319.55m, 因此1000m（1KM）在经线上对应约为1000 / 111319.55=0.009度，这个度数和纬度没有关系
         * 在纬线上，相差一经度约为111319.55 * cos(α)（α为该纬线的纬度）, 因此1000m（1KM）在该纬线上对应约为
         * 1000 / (111319.55 * cos(a))度，这个度数和纬度有关
         */
        Set<String> ret = new HashSet<>();
        int numberOfBlock = (int) (2.0 * radius / GEO_FENCING_PRECISION[hashLength - 1]);
        double topLatitude = latitude + radius / METERS_PER_LATITUDE;
        double LatitudePerScale = 2.0 * radius / METERS_PER_LATITUDE / numberOfBlock;

        for (int i = 0; i < numberOfBlock; i++) {
            double tmpLatitude = topLatitude - i * LatitudePerScale;
            double t1 = Math.cos(Math.toRadians(tmpLatitude));
            double leftLongitude = longitude - (radius / (METERS_PER_LATITUDE * t1));
            double LongitudePerScale = 2.0 * radius / (METERS_PER_LATITUDE * t1) / numberOfBlock;
            Set<String> res = new HashSet<>();
            for (int j = 0; res.size() < numberOfBlock; j++) {
                double tmpLongitude = leftLongitude + j * LongitudePerScale;
                String geoHash = geoHash(tmpLongitude, tmpLatitude, hashLength);
                res.add(geoHash);
            }
            ret.addAll(res);
        }
        return ret;
    }
    /**
     * 获得地球上两点间距离。
     *
     * @param lng1
     * @param lat1
     * @param lng2
     * @param lat2
     * @return
     */
    public static double getDistance(double lng1, double lat1, double lng2, double lat2) {
        double radLat1 = rad(lat1);
        double radLat2 = rad(lat2);
        double a = radLat1 - radLat2;
        double b = rad(lng1) - rad(lng2);

        double s = 2 * Math.asin(Math.sqrt(Math.pow(Math.sin(a / 2), 2) +
                Math.cos(radLat1) * Math.cos(radLat2) * Math.pow(Math.sin(b / 2), 2)));
        s = s * EARTH_RADIUS;
        s = Math.round(s * 10000) / 10000;
        return s;
    }

    private static double rad(double d) {
        return d * Math.PI / 180.0;
    }

    private static String getCode(double value, double min, double max, int length) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < length; i++) {
            double middle = (min + max) / 2;
            if (value >= middle) {
                sb.append(1);
                min = middle;
            } else {
                sb.append(0);
                max = middle;
            }
        }
        return sb.toString();
    }
}
